For any suggestions or feedback regarding these notes,
please contact Pragy Agarwal
Imagine that you're building Zomato, and you've a table called restaurants that holds 100 million entries.
create table restaurants(id bigint primary key,
name varchar(50),
address varchar(200),
latitude real,
longitude real)
Assume that we also have the necessary indexes on the latitude and longitude columns.
Note: modern SQL databases like postgres support geolocation datatypes via extensions (eg, postgis)
Given a user's geolocation (latitude, longitude), you have to find k nearby restaurants.
select *,
((latitude - user_latitude) * (latitude - user_latitude)
+ (longitude - user_longitude) * (longitude - user_longitude)) as distance
from restaurants
order by distance asc
limit k
note: the formula used is the Euclidean Distance (two point distance) formula
O(N log k)
Given an unsorted array of n elements, find the top k ⇒ this takes O(n log k) time
select *,
((latitude - user_latitude) * (latitude - user_latitude)
+ (longitude - user_longitude) * (longitude - user_longitude)) as distance
from restaurants
where distance <= R
limit k
O(N)
No sorting/heap needed now.
But we still have to perform a linear scan through the entire table – because the distance formula is complex, and the database cannot use the index to optimize this formula.
A database index is just a sorted array / BBST
You can do lowerbound/upperbound queries (binary search) using an index.
Circle formulas are complex. Rectangles are simpler!
select *
from restaurants
where user_latitude between ( latitude - L) and ( latitude + L)
and user_longitude between (longitude - L) and (longitude + L)
limit k
O(log N)
still O(N) worst case! Because you can only use 1 index at a time.
We are still searching through a much larger region than what we actually desire!
Note: this query is better than the circle query, but still not perfect!
create index idx_lat on restaurants(latitude);
create index idx_lon on restaurants(longitude);
create index idx_lat_lon on restaurants(latitude, longitude);
create index idx_lon_lat on restaurants(longitude, latitude);
Unfortunately, even after having multi-column indexes, the search will still remain the exact same! (we will still scan through the blue+yellow region, and then take intersection)
Why though?
Multi-column indexes can perform range queries only on last column!
Demo - why Composite Index can't do multi-column range queries
alter table restaurants add cell_id integer;
get_cell_id(user_latitude, user_longitude)
select *
from restaurants
where cell_id = get_cell_id(user_latitude, user_longitude)
finally, this query will achieve our desired complexity!
O(log N)
Density of restaurants changes depending on the location.
Some cells have lots of restaurants, some cells have no restaurants.
So if the user happens to fall in a low density region, we will not be able to find sufficient restaurants for this user.
One approach to fixing this could be to simply expand the search to the nearby cells
select * from restaurants
where cell_id in ( 18, 17, 19, 5, 6, .. ) (surrounding cells)
We don't want a fixed sized grid - we want a Dynamic Sized Grid!
We need a grid that considers the density of the region.
Note: There are many other types of spatial indexes.
Visualization: https://kshitijmishra23.github.io/interactive-quadtrees/
QuadTrees provide efficient range queries on 2 dimensions simultaneously!
Unlike regular B+tree indexes, which only work in 1 dimension, quad-trees work in 2 dimensions!
M is a configurable parameter
Each node in the tree must be aware of its coordinates.
Please practice the QuadTree data structure: Construct Quad Tree - LeetCode
class Restaurant:
id: int
latitude: float
longitude: float
# the rest of the info will be stored in the db table, and not in the quad tree
class QuadTree:
id: int
top, left, bottom, right: (float, float, float, float)
restaurants: list[Restaurant] # this will be populated
# only for leaf nodes
# it will be empty for intermediate nodes
children: list[QuadTree] # populated only for intermediate nodes
# it will be empty for leaf nodes
def find(self,
latitude: float,
longitude: float) -> list[Restaurant]:
"""given a user's location, finds the restaurants that
fall in the same cell as this location"""
if not ( self.left <= latitude < self.right \
and self.top <= longitude < self.bottom ):
return [] # user's location out of current node's bounds
if not self.children: # we're a leaf node
return self.restaurants
# recurse over children
for child in self.children:
restaurants = child.find(latitude, longitude)
if restaurants:
return restaurants
def insert(self, restaurant: Restaurant):
"""insert the restaurant"""
if not ( self.left <= restaurant.latitude <= self.right \
and self.top <= restaurant.longitude <= self.bottom ):
return # location out of current node's bounds
if not self.children: # we're a leaf node
self.restaurants.append(restaurant)
if len(self.restaurants) > SPLIT_THRESHOLD:
# create 4 new children
# with the appropriate coordinates
# move restaurants to children
...
return
# recurse over children
for child in self.children:
child.insert(restaurant)
def delete(self, restaurant: Restaurant):
"""delete the restaurant from the tree"""
# similar to insert
# find the node
# if leaf remove restaurant from the list
# if parent’s count < SPLIT_THRESHOLD, then go to parent
# and re-join children
# otherwise, recurse on our children
Note that
Also note that a leaf node need not always contain M restaurants.
It is possible for a leaf node to be empty.
If a user happens to fall in a cell which doesn't contain any restaurants, how will we find the relevant restaurants?
O(log4 N) where N is the number of entries in the quadtree
Note that this is the average complexity of insert/delete/find
The worst case can be very bad up to O(N) because the tree can get skewed — but practically this is unlikely to happen.
Practically, the complexity still remains O(log4 N)
If we have 100 million restaurants, how large (in bytes) will the quadtree be?
class Restaurant:
id: int
latitude: float
longitude: float
# note that other details about the restaurant will not
# be stored within the quad tree
# they will be stored in the SQL table
class QuadTree:
id: int
top, left, bottom, right: (float, float, float, float)
restaurants: list[Restaurant]
children: list[QuadTree]
Only the leaf nodes will store the restaurants. Intermediate nodes do NOT store the restaurants.
(overestimating the number of nodes)
Assume each leaf node stores 1 restaurant on average..
But we can assume that each leaf has at least 1 restaurant on average.
We will need around 12 GB to store a quadtree with 100 million restaurants
Absolutely not!
11 GB is actually small enough to fit in the RAM
Typical production servers can have up to 64GB of RAM easily..
A single server can hold the entire quad tree to store 100 million nodes.
Because the complexity of operations is O(log4 N) and all operations are happening purely in the RAM, the single server will be able to handle 10,000 - 100,000 operations / second!
The quadtree is not storing any additional info - every piece of info that it needs is already present in the SQL db.
If the server crashes, we can just spin up another QuadTree server and rebuild the tree from scratch
Rebuilding the entire tree will be an O(N log4 N) operation - it will only take a few seconds at max to rebuild.
N = 100M
N log4 N = 100M * 13.xyz
210 = 1024 ~ 1000 = 103
210 ~ 103 (1K)
220 ~ 106 (1M)
226 ~ 64M
227 ~ 128M
log2(100M) ~ 26.xyz
log4(100M) ~ 13.xyz
Construct Quad Tree - LeetCode